本系列文章已重新編修,並在加入部分 ES6 新篇章後集結成書,有興趣的朋友可至天瓏書局選購,感謝大家支持。
購書連結 https://www.tenlong.com.tw/products/9789864344130
讓我們再次重新認識 JavaScript!
今天在 Facebook 的前端社團看到有網友問了一個有趣的問題
看到這個問題,我想大家應該都會很直覺地想到 beforeunload
以及 unload
事件吧。
說到這個 beforeunload
及 unload
就想來問問各位,使用過前端 MVVM 框架開發的朋友都知道 Component 都有生命週期的觀念,但你知道其實網頁也有生命週期嗎?
DOMContentLoaded
與 load
事件還記得我們曾在 重新認識 JavaScript: Day 12 透過 DOM API 查找節點 以及 重新認識 JavaScript: Day 16 那些你知道與不知道的事件們 一文中有提到過,當瀏覽器在載入網頁時,瀏覽器會先分析這個 HTML 檔案且「由上而下」依序來讀取解析網頁的內容:
所以當我們嘗試著在 <head> ... </head>
裡面的 <script>
內去存取 DOM 的內容,實際上是無法的,因為此時 DOM 結構尚未形成。
所以此時我們就必須要利用 DOMContentLoaded
或 load
事件,來確保 DOM 結構被完整的讀取跟解析。
document.addEventListener("DOMContentLoaded", function(){
// DOM Ready!
});
或是
window.addEventListener("load", function(event) {
// All resources finished loading!
});
兩者的差異在前面也曾提過, load
事件是在網頁「所有」資源都已經載入完成後才會觸發,而 DOMContentLoaded
事件是在 DOM 結構被完整的讀取跟解析後就會被觸發,不須等待外部資源讀取完成。換言之, load
事件會在 DOMContentLoaded
之後才被觸發,而這兩個事件也都可以確保網頁結構載入完成。
看到這裡,也許你會想起 jQuery 的 ready()
function。
沒錯,事實上 jQuery.ready()
做的事與 DOMContentLoaded
是一樣的,差別只在於針對老舊瀏覽器的支援程度。 讓我們來看看 jQuery 的原始碼片段:
jQuery.ready.promise = function( obj ) {
if ( !readyList ) {
readyList = jQuery.Deferred();
// Catch cases where $(document).ready() is called after the browser event has already occurred.
// we once tried to use readyState "interactive" here, but it caused issues like the one
// discovered by ChrisS here: http://bugs.jquery.com/ticket/12282#comment:15
if ( document.readyState === "complete" ) {
// Handle it asynchronously to allow scripts the opportunity to delay ready
setTimeout( jQuery.ready );
// Standards-based browsers support DOMContentLoaded
} else if ( document.addEventListener ) {
// Use the handy event callback
document.addEventListener( "DOMContentLoaded", completed, false );
// A fallback to window.onload, that will always work
window.addEventListener( "load", completed, false );
// If IE event model is used
} else {
// Ensure firing before onload, maybe late but safe also for iframes
document.attachEvent( "onreadystatechange", completed );
// A fallback to window.onload, that will always work
window.attachEvent( "onload", completed );
// If IE and not a frame
// continually check to see if the document is ready
var top = false;
try {
top = window.frameElement == null && document.documentElement;
} catch(e) {}
if ( top && top.doScroll ) {
(function doScrollCheck() {
if ( !jQuery.isReady ) {
try {
// Use the trick by Diego Perini
// http://javascript.nwbox.com/IEContentLoaded/
top.doScroll("left");
} catch(e) {
return setTimeout( doScrollCheck, 50 );
}
// detach all dom ready events
detach();
// and execute any waiting functions
jQuery.ready();
}
})();
}
}
}
return readyList.promise( obj );
};
由於 IE 在 IE8 以前是沒有 DOMContentLoaded
這個事件,所以可以看到 jQuery 為了確保瀏覽器的完整相容性,透過各種不同的方式來實作 ready
。
如果瀏覽器支援 DOMContentLoaded
的話,就會直接註冊 DOMContentLoaded
事件,並且加上 load
事件來作為 fallback 保險:
else if ( document.addEventListener ) {
// Use the handy event callback
document.addEventListener( "DOMContentLoaded", completed, false );
// A fallback to window.onload, that will always work
window.addEventListener( "load", completed, false );
}
如果是舊版本的 IE,則透過 attachEvent
加上 onreadystatechange
、 onload
來確保相容性:
// Ensure firing before onload, maybe late but safe also for iframes
document.attachEvent( "onreadystatechange", completed );
// A fallback to window.onload, that will always work
window.attachEvent( "onload", completed );
另外,由於判斷 document.readyState
與 readyStateChange
的時機點會有誤差,所以 jQuery 利用了不斷執行 doScrollCheck()
來檢查 DOM 是否確實載入完成。
beforeunload
與 unload
講完 load
的部分之後,再回到這篇一開始講的 beforeunload
與 unload
。
當使用者嘗試要關閉網頁、點擊了網頁的連結,或是要往上/下一頁、甚至重新整理頁面的時候,就會觸發這類事件。
兩者的差別在於, beforeunload
是在網頁被卸載「之前」觸發,而 unload
是在網頁被卸載「之後」觸發,所以如果我們想要跳出警告視窗提醒使用者是否離開,就得在 beforeunload
事件處理,而不是 unload
,因為此時網頁已經離開。
值得一提的是,過去我們在 beforeunload
事件可以自訂提示訊息,這個功能在 Chrome v51 (2016/04) 時被取消了,理由是防止 beforeunload
的自訂訊息被用來做為詐騙用途。 詳情請見 Remove custom messages in onbeforeunload dialogs。
回歸主題,所以網頁的生命週期,若是以「事件」來區分,大致上可以分成幾個部分:
如果是 document.readyState
階段來區分,則可以分成
最後,回到一開始網友的問題,若想要在「點擊瀏覽器的關閉才觸發的事件」在實務上是不可能的,因為當使用者嘗試要關閉網頁、點擊了網頁的連結,或是要往上/下一頁、甚至重新整理頁面的時候,都會觸發 beforeunload
。
但若是只想要避開點擊網頁連結的話,則是可以在 <a>
的 click
事件加上 window.onbeforeunload = null;
來取消 beforeunload
,警告視窗就不會跳出來煩人了。